<?php
class DevelTypoStrategy implements ITypoStrategy {
	//rus_abc    = 'А-Яа-я';
	const rusAbc = 'йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮЁё';
	const engAbc = 'A-Za-z';	
	const etcLetter = '';
	
    public function getSafeBlocksRegex() {
        return '~((?U)' . $this->composeOrRegex($this->getSafeBlocks()) .  ')~is';
    }

    public function getProcessRegexRules() {

        return array(

            $this->returnRules(array_merge(
                $this->getSafeAmbiguousRules(),
                $this->getSpecSymbolsRules(),
                $this->getMisprintRules()
            )),

            $this->returnRules(array_merge(
                $this->getQuoteRules(),                
                $this->getEtcRules()
            )),

            $this->returnRules(array_merge(
                $this->getNowrapRules()
            )),

        );
    }

    protected final function returnRules($hash) {
        return array(array_keys($hash), array_values($hash));
    }

    protected function getSafeBlocks() {
        return array(
            '<pre[^>]*>.*<\/pre>',
            '<style[^>]*>.*<\/style>',
            '<script[^>]*>.*<\/script>',
            '<!--.*-->',
            '<[^>]*[\s][^>]*>',
        );
    }

    protected function getEntity($name) {

        $hash = $this->getBaseEntities();

        if (empty($hash[$name]))
            throw new Exception("Unknown entity - '$name'");

        return $hash[$name];
    }

    protected function getNbsp() {
        return '&nbsp;';
    }

    protected function getPunctiuation() {
        return '[?!:,]';
    }

    protected function getAnyLetter() {
        return  '['.self::rusAbc.self::engAbc.self::etcLetter.']';
    }
    
    protected function getNonLetter() {
        return '[^' . $this->getAnyLetter() . ']';
    }

    protected function getHtmlTag() {
        //return '(?:(?U)<.*>)'; // иногда не работает. Не локализовано.
        return '(?:<[^>]*>)';
    }

    protected function getPhraseEnd() {

        return $this->composeOrRegex(array(
            '[)!?.:;#*\\\]',
            '$',
            '\w',
            $this->getAnyLetter(),
            $this->getEntity('rquote'),
            $this->getEntity('rquote2'),
            '&quot;',
            '"',
            "'",
            $this->getEntity('hellip'),
            $this->getEntity('copy'),
            $this->getEntity('trade'),
            $this->getEntity('apos'),
            $this->getEntity('reg'),
            //todo: is this really needed?
            '\\\\'
        ));
    }

    protected function getHellip() {
        return '\.{2,5}';
    }
    protected function getPhraseBegin() {
        return $this->composeOrRegex(array(
            $this->getHellip(),
            '\w',
            "\n",
            //$this->getAnyLetter(),
        ));
    }

    protected function getBaseEntities() {

        return array(
            'lnowrap' => '<span style="white-space:nowrap">',
            'rnowrap' => '</span>',

            'lquote'  => '«',
            'rquote'  => '»',
            'lquote2' => '‘',
            'rquote2' => '’',
            'inch'    => '&#34;', // _real_ inch

            'mdash'   => '—',
            'ndash'   => '–',
            'minus'   => '−', // соотв. по ширине символу +, есть во всех шрифтах

            'hellip'  => '…',
            'copy'    => '©',
            'trade'   => '™',
            'apos'    => '&#39;',  // http://fishbowl.pastiche.org/2003/07/01/the_curse_of_apos

          //'reg'     => '®',
            'reg'     => '<sup><small>®</small></sup>',

            'multiply' => '&times;',
            '1/2' => '&frac12;',
            '1/4' => '&frac14;',
            '3/4' => '&frac34;',
            'plusmn' => '&plusmn;',
            'rarr' => '&rarr;',
            'larr' => '&larr;',
        );
    }

    protected function getQuoteRules() {

        $anyQuote = $this->composeOrRegex(array(
            $this->getEntity('lquote'),
            $this->getEntity('rquote'),
            $this->getEntity('lquote2'),
            $this->getEntity('rquote2'),
            '&quot;',
            '"',
        ));

        $htmlTag = $this->getHtmlTag();
        $phraseEnd = $this->getPhraseEnd();
        $phraseBegin = $this->getPhraseBegin();

        return array(
             // Превращаем кавычки в ёлочки. Двойные кавычки склеиваем.
             "~(?<=\s|^|[>(])($htmlTag*)($anyQuote+)($htmlTag*$phraseBegin$htmlTag*)~" => '$1'.$this->getEntity('lquote').'$3',
             "~($htmlTag*$phraseEnd$htmlTag*)($anyQuote+)($htmlTag*$phraseEnd$htmlTag*|\s|[,<-]|$)~"   => '$1'.$this->getEntity('rquote').'$3',
        );
    }

    protected function getSpecSymbolsRules() {

        return array(

            '~\((c|с)\)~iu'  => $this->getEntity('copy'),
            '~\(r\)~i'  =>  $this->getEntity('reg'),
            '~\(tm\)~i' =>  $this->getEntity('trade'),

            '~\b1/2\b~' => $this->getEntity('1/2'),
            '~\b1/4\b~' => $this->getEntity('1/4'),
            '~\b3/4\b~' => $this->getEntity('3/4'),

            // Размеры 10x10, правильный знак
            '~(\d+)[x|X|х|Х|*](\d+)~u' => '$1' . $this->getEntity('multiply') . '$2',

            //+-
            '~([^\+]|^)\+-~' => '$1' . $this->getEntity('plusmn'),

            //arrows
            '~([^-]|^)->~' => '$1' . $this->getEntity('rarr'),
            '~<-([^-]|$)~' => $this->getEntity('larr') . '$1',

        );
    }

    protected function getNowrapRules() {

        $abbr = $this->composeOrRegex(array(
            'ЗАО',
            'ИП',
            'НИИ',
            'НПФ',
            'ОАО',
            'ООО',
            'ЧП',
        ));

        $prepos = $this->composeOrRegex(array(
            'а',
            'без', 'безо',
            'в', 'во', 'вне',
            'да', 'для','до',
            'ещё',
            'за',
            'и', 'или', 'из', 'изо', 'их', 'или',
            'к', 'как', 'ко',
            'меж', 'между',
            'на', 'не', 'ни', 'но', 'над', 'надо',
            'о', 'об', 'обо', 'от', 'ото', 'около',
            'при', 'перед','передо', 'по', 'под', 'подо', 'пред', 'предо', 'про',
            'с', 'со', 'сквозь',
            'там', 'то',
            'у', 'уж',
            'через', 'что',
            'я'
        ));
        
        //TODO: дополнить
        $metrics = $this->composeOrRegex(array(
        	'мм', 'см', 'м', 'км',
        	'мг', 'г', 'кг',
        	'б', 'кб', 'мб', 'гб',
        ));
        
        //TODO: дополнить
        $shortages = $this->composeOrRegex(array(
        	'[гГ]', 'пос', 'c', 'ул', 'д', 'пер', 'м',
        	'гр', 'тов'
        ));

        $particles = $this->composeOrRegex(array(
            'б', 'бы',
            'ж', 'же',
            'или',
            'ли', 'либо', 'ль',
        ));

        $htmlTag = $this->getHtmlTag();

        $laquo = $this->composeOrRegex(array(
            $this->getEntity('lquote'),
            $this->getEntity('lquote2'),
            '"',
        ));

        $raquo = $this->composeOrRegex(array(
            $this->getEntity('lquote'),
            $this->getEntity('lquote2'),
            '"',
        ));

        return array(
             //Нельзя отрывать имя собственное от относящегося к нему сокращения.
             //Например: тов. Сталин, г. Воронеж
             //Ставит пробел, если его нет.
             '~'.$shortages.'\.\s*([А-Я]+)~su' => '$1.'.$this->getNbsp().'$2',

             //Не отделять стр., с. и т.д. от номера.
             '~(стр|с|табл|рис)\.\s*(\d+)~sui' => '$1.'.$this->getNbsp().'$2',

        	//TODO: review
            //Не разделять 2007 г., 8 лет
            '~([0-9]+)\s*(г\.|лет|год[ау]м?)~sui' => '$1'.$this->getNbsp().'$2',

//          FIXME ставит nbsp где не надо
            // приклеиваем предлоги и союзы к последующему слову
            //'/(?<=\s|^|\W)(' . $prepos . ')(\s+)/iu' => '$1' . $this->getNbsp(),
            //"/(?<=" . $this->getNonLetter() ." |^)($prepos)(\s+)/iu" => '$1' . $this->getNbsp(),
            //"/(" . $this->getNonLetter() . "|^)($prepos)(\s+)/iu" => '$1$2' . $this->getNbsp(),
            "/(\s|^)($prepos)(\s+)/iu" => '$1$2'.$this->getNbsp(),

             //Неразрывные названия организаций и абревиатуры форм собственности
            '~('.$abbr.')\s+'.$laquo.'(.*)'.$raquo.'~' => $this->getEntity('lnowrap') . '$1 "$2"' . $this->getEntity('rnowrap'),

            // Нельзя отрывать частицы бы, ли, же от предшествующего слова, например: как бы, вряд ли, так же.
            //FIXME: не работет
            "~(?<=\S)(\s+)($particles)(?=$htmlTag*[\s)!?.])~ui" => $this->getNbsp() . '$2',

            // Неразрывный пробел после инициалов.
            '~('.$this->getAnyLetter().'\.)\s?('.$this->getAnyLetter().'\.)\s?('.$this->getAnyLetter().'+)~su' => '$1$2' . $this->getNbsp() . '$3',

            // Неразрывные пробелы если в кавычках 2 слова
            // FIXME conflict 'рассказы [«Сердце»,•«Эвакуация»] и•[«Майский•жук»].'
            '~(' . $this->getEntity('lquote') . '[\S"]*)(\s+)([\S"]*' . $this->getEntity('rquote') . ')~U' => '$1' . $this->getNbsp() . '$3',

            // Русские денежные суммы, расставляя пробелы в нужных местах.
            '~(\d+)\s?(руб.)~sui' =>  '$1' . $this->getNbsp() . '$2',
            '~(\d+)\s?(млн.|тыс.)?\s?(руб.)~sui'  =>  '$1' . $this->getNbsp() . '$2' . $this->getNbsp() . '$3',
        
			//Неразрывный пробел между цифрой и единицей измерения
			'~([0-9]+)\s*('.$metrics.'(?:\.|\s))~s' => '$1'.$this->getNbsp().'$2',
        );
    }

    protected function getSafeAmbiguousRules() {
        return array(
            // safe inches
            '~(\s\d+|\s\d+[.,]\d+)"([,]?[\s]+|$)~' => '$1' . $this->getEntity('inch') . '$2',
        );
    }

    protected function getMisprintRules() {

        //Знаки препинания (троеточие и точка - отдельный случай!)
        $punctuation = $this->getPunctiuation();
        $anyLetter = $this->getAnyLetter();

        return array(        
        	// Оторвать скобку от слова
			'~('.$anyLetter.')\(~u' => '$1 (',

			// Оторвать тире от слова
			'~('.$anyLetter.')- ~u' => '$1 - ',

            // Много пробелов или табуляций -> один пробел
            '~( |\t)+~u' => ' ',

            //Знаки с предшествующим пробелом... нехорошо!
            '~(' . $this->getPhraseEnd() . ') +(' . $punctuation . '|' . $this->getEntity('hellip') . '|' . $this->getHellip() . ')~' => '$1$2',

            // conflict:  ' 1,5 ' => ' 1, 5 '
            '~('.$punctuation.')('.$this->getPhraseBegin().')~' => '$1 $2',


             // Разносим неправильные кавычки
            //'~([^"]\w+)"(\w+)"~u' => '$1 "$2"',
            '~(' . $anyLetter .'+)"(' . $anyLetter . '+)"~u' => '$1 "$2"',
            '~"(' . $anyLetter . '+)"(' . $anyLetter . '+)~u' => '"$1" $2',

            //Слепляем скобки со словами
            '~\(\s~us' => '(',
            '~\s\)~us' => ')',
        );
    }

    protected function getEtcRules() {
        return array(
            // Знак дефиса или два знака дефиса подряд - на знак длинного тире.
            '~(\s+|^)(--?)(?=\s)~' => '$1' . $this->getEntity('mdash'),

            // Знак дефиса, ограниченный с обоих сторон цифрами - на знак короткого тире.
            '~(?<=\d)-(?=\d)~' => $this->getEntity('ndash'),

            // От 2 до 5 знака точки подряд - на знак многоточия (больше - мб авторской задумкой).
            '~' . $this->getHellip() . '~' => $this->getEntity('hellip'),

            "~'~" =>  $this->getEntity('apos'),

        //    //Телефоны
        //    '~тел[:.] ?(\d+)~ie' => "'<span style=\"white-space:nowrap\">тел: '.self::_phone('$1').'</span>'",
        	'~(?:тел\.?/?факс:?\s?\((\d+)\))~iu' => 'тел./факс:'.$this->getNbsp().'($1)',


            //Номер версии программы пишем неразрывно с буковкой v.
            '~([vв]\.)\s?([0-9])~iu' => '$1'.$this->getNbsp().'$2',
            '~('.$this->getAnyLetter().') ([vв]\.)~iu' => '$1'.$this->getNbsp().'$2'
        );
    }

    // util
    protected function composeOrRegex(array $arrPatterns) {
        return '(?:' . implode('|', $arrPatterns) . ')';
    }
}